简单的程序可能只需要两个角色(普通用户和管理员),对于这种情况,在User模型中添加一个is_administrator布尔值字段就足够了。

复杂的用户角色可以通过赋予用户某些权限的组合是一种很好的方法。

9.1 角色在数据库中的表示

1. 在app/models.py中增加用户权限字段:

1
2
3
4
5
6
7
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
default = db Column(db.Boolean, default=False, index=True)
permission = db.Column(db.Integer)
users = db.relationship('User', backref='role', lazy='dynamic')
  • deafult字段中的default参数如果设置为True,则该角色为默认角色,用户注册时会默认为该角色(具体实现是给defalut赋值True)。
  • permission字段为用户权限,其值是一个整数。各权限位值表如下:
    表9-1 程序的权限
操作 位值 说明
关注用户 0b00000001 (0x01) 关注其他用户
在他人的文章中发表评论 0b00000010(0x02) 在他人撰写的文章中发布评论
写文章 0b00000100(0x04) 写原创文章
管理他人发表的评论 0b00001000(0x08) 查处他人发表的不当评论
管理员权限 0b10000000(0x80) 管理网站

2. 在app/models.py中定义权限常量:

1
2
3
4
5
6
class Permission:
FOLLOW = 0x01
COMMENT = 0x02
WRTIE_ARTICLES = 0x04
MODERATE_COMMENTS = 0x08
ADMINISTER = 0x80

一般网站的用户角色以及对应拥有的权限如表9-2:

表9-2 用户角色

用户角色 权限 说明
匿名 0b00000000(0x00) 未登录的用户,在程序中只有阅读权限
用户 0b00000111(0x07) 具有关注其他用户、发表评论和发布文章的权限。这是新用户的默认角色
协管员 0b00001111(0x0f) 增加审查不当评论的权限
管理员 0b11111111(oxff) 具有所有权限,包括修改其他用户所属角色的权限

这样就可以根据权限的组合来组织角色。

3. 在app/models.py中支持创建角色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# ...
class Role(db.Model):
# ...
@staticmethod
def inset_roles():
roles = {
'User': [Permission.FOLLOW, Permission.COMMENT,
Permission.WRITE_ARTICLES],
'Moderator': [Permission.FOLLOW, Permission.COMMENT,
Permission.WRITE_ARTICLES, Permission.MODERATE_COMMENT],
'Anministrator': [Permission.FOLLOW, Permission.COMMENT,
Permission.WRITE_ARTICLES,Permission.MODERATE_COMMENT,
Permission.ADMINISTER]
}
default_role = 'User'
for roel in roles:
# 查找Role表中是否有name=role这角色
role = Role.query.filter_by(name=role).first()
# 如果没有则创建
if role is None:
role = Role(name=role)
# 将permissio字段的值设为0
role.reset_permission()
# 添加权限
for perm in roles[role]:
role.add_permission(perm)
# 判断role.name是否为默认角色'User',如果是则将role.default赋值为True
role.default = (role.name == default_role)
# 将数据库变化添加到会话中
db.session.add(role)
# 提交数据库变化
db.session.commit()

insert_roles()函数并不直接创建新角色对象,而是先通过for循环遍历roles,查询Role表中是否有该角色,当表中没有这个角色色才创建角色。

4. 适用shell会话把角色写入数据库:

1
2
3
4
(venv) $ python flasky.py shell
>>> Role.insert_roles()
>>> Role.query.all()
[<Role u'Administrator'>, <Role u'User'>, <Role u'Moderator'>]

9.2 赋予角色(在用户注册账户时赋予适当的角色)

1. 在app/models.py中定义默认赋予的用户角色:

1
2
3
4
5
6
7
8
9
10
11
12
13
# ...
class User(UserMixin, db.Model):
# ...
def __init__(self, **kwargs):
super(User, self).__init__(**kwargs)
if self.role is None:
# 如果email邮箱是管理员邮箱,则赋予‘管理员’角色,否则赋予‘默认’角色
if self.email == current_app.config['FLASKY_ADMIN']:
self.role = Role.query.filter_by(name='Administrator').first()
if self.role is None:
self.role = Role.query.filter_by(dafault=True).first()

User类的构造函数首先调用基类的构造函数,如果创建基类对象后还没有定义角色(即roleNone),则根据电子邮件决定将其设为管理员还是默认角色。

9.3 角色验证(检查是否有指定权限)

1. 在app/models.py中检查用户是否有指定的权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from flask_login import UserMixin, AnonymousUserMixin
# ...
class User(UserMixin, db.model):
# ...
def can(self, permissions):
return self.role is not None and \
# 进行位与运算
(self.role.permission & permissions) == permissions
def is_administrator(self):
return self.can(Permission.ADMINSTER)
# 定义匿名用户类
class AnonymousUser(AnonymousUserMixin):
def can(self, permissions):
return False
def is_administrator(self):
return False
# 实例化AnonymousUser类(即设置用户未登录时current_user的值,
# 这样程序就不用先检查用户是否登录,都能调用current_user.can()
# 和current_user.is_administrator())
login_manager.anonymous_user = AnonymousUser

2. 在app/decorators.py中定义用于检查用户权限的修饰器:

如果想让视图函数只对具有特定权限的用户开放,可以适用自定义的修饰器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from functools import wraps
from flask imoprt abort
from flask_login import current_user
from app.models import Permission
def permission_required(permission):
def decorator(func):
@wraps(func)
def decorated_function(*args, **kwargs):
# 判断用户是否具有某特定权限,如果没有则抛出403错误
if not current_user.can(permission):
abort(403)
return func(*args, **kwargs)
return decorated_function
return decorator
def admin_required(func):
# 相当于调用decorator(func)
return permission_required(Permission.ADMINSTER)(func)

3. 在app/main/__init__.py中把Permission类加入模板上下文:

在模板中可能也需要检查权限,所以Permission类为所有为定义了常量以便获取。

为了避免每次调用render_template()时多添加一个模板参数,可以使用上下文管理器,使得变量在所有模板中全局可访问。

1
2
3
4
5
6
7
# ...
from app.models import Permission
@main.app_context_processor
def inject_permission():
# 实例化Permission类,并将其字典化
return dict(Permission=Permission)

4. 在tests/test_user_model.py中添加角色和权限的单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ...
class UserModelTestCase(unittest.TestCase):
# ...
def test_user_role(self):
u = User(email='john@example.com', password='cat')
self.assertTrue(u.can(Permission.FOLLOW))
self.assertTrue(u.can(Permission.COMMENT))
self.assertTrue(u.can(Permission.WRITE))
self.assertFalse(u.can(Permission.MODERATE))
self.assertFalse(u.can(Permission.ADMIN))
def test_moderator_role(self):
r = Role.query.filter_by(name='Moderator').first()
u = User(email='john@example.com', password='cat', role=r)
self.assertTrue(u.can(Permission.FOLLOW))
self.assertTrue(u.can(Permission.COMMENT))
self.assertTrue(u.can(Permission.WRITE))
self.assertTrue(u.can(Permission.MODERATE))
self.assertFalse(u.can(Permission.ADMIN))

注意:还是那句话,修改数据库模型之后,记得先执行python flasky.py db migrate -m "blablabla"生成数据库迁移脚本,再执行python flasky.py db upgrade更新数据库。